#include "Communication.h"
#include <assert.h>
#include <Codec.h>
#include <JsonParse.h>
#include <RsaCrypto.h>
#include <netinet/in.h>
#include <memory>
#include <Log.h>

// 实例化Communication: 初始化[MySQL连接对象], 初始化[连接Redis服务器的实例]
Communication::Communication() : _mysql(new MySqlConn) {
    JsonParse json;
    shared_ptr<DBInfo> dbInfo = json.getDataBaseInfo(JsonParse::Mysql);
    bool flag = _mysql->connect(dbInfo->_user, dbInfo->_dbName, dbInfo->_password,
                                    dbInfo->_ip, dbInfo->_port);
    assert(flag);

    // 初始化[连接Redis服务器的实例]
    _redis = new Room;
    flag = _redis->initEnvironment();
    assert(flag);
}

Communication::~Communication() {
    if (_redis) { delete _redis; }
    if (_aes) { delete _aes; }
    if (_mysql) { delete _mysql; }
}


// 解析客户端发送过来的数据
void Communication::parseRequest(Buffer* buf) {
    // 网络数据 = 数据头 + 数据块 -> 先读数据头
    string data = buf->data(sizeof(int));          // data = 数据头
    int length = *(reinterpret_cast<int*>(data.data())); // length = 数据头记录的数据块长度(大端序列)
    length = ntohl(length);

    // 读数据块
    data = buf->data(length); // data = 数据块
    if (_aes) { // 若服务器已获取Aes密钥, 则客户端发送的数据会用Aes进行加密, ∴ 服务器需要用Aes进行解密
        data = _aes->deCrypto(data);
    }

    // 反序列化[请求数据] -> 处理请求, 并且实例化[回复数据]
    Codec codec(data);
    Message resMsg; // 回复数据
    callback sendFunc = _sendMessage; // 用于发送数据: 根据情况, _sendMessage()给当前用户发送数据, readyForPlay()给同一房间的所有玩家发送数据
    shared_ptr<Message> ptr = codec.decodeMsg();
    switch (ptr->_reqCode) {
        case RequestCode::AesFenFa :
            /* 处理客户端发来的[Aes密钥分发的Message]
             *      [1] 用私钥解密[Message._data1 = 用公钥加密的Aes秘钥], 获取Aes的密钥
             *      [2] 进行Aes密钥的哈希校验
             *      [3] 根据[哈希校验的结果]准备给[客户端回复的数据]
            */
            handleAesFenFa(ptr.get(), resMsg);
            break;
        case RequestCode::UserLogin :
            handleLogin(ptr.get(), resMsg);
            break;
        case RequestCode::Register :
            handleRegister(ptr.get(), resMsg);
            break;
        case RequestCode::AutoRoom :
        case RequestCode::ManualRoom :
            handleAddRoom(ptr.get(), resMsg);
            sendFunc = bind(&Communication::readyForPlay, this, resMsg._roomName, placeholders::_1);
            break;
        case RequestCode::GrabLord : // 房间中有玩家抢地主
            // 告知房间内的其他玩家: 有玩家下注了x分, 来抢地主
            resMsg._data1 = ptr->_data1; // _data1 = 玩家下注的分数
            resMsg._resCode = OtherGrabLord;
            sendFunc = bind(&Communication::notifyOtherPlayers, this, std::placeholders::_1, ptr->_roomName, ptr->_userName);
            break;
        case RequestCode::PlayAHand : // 房间中有玩家出牌了
            resMsg._data1 = ptr->_data1; // _data1 = 所出牌的数量
            resMsg._data2 = ptr->_data2; // _data2 = 所出牌(经QDataStream序列化)
            resMsg._resCode = RespondCode::OtherPlayHand;
            sendFunc = bind(&Communication::notifyOtherPlayers, this, std::placeholders::_1, ptr->_roomName, ptr->_userName);
            break;
        case RequestCode::GameOver : // 游戏结束了, 玩家发来自己的分数, 客户端要记录分数到redis、mysql
            handleGameOver(ptr.get());
            sendFunc = nullptr;
            break;
        case RequestCode::Continue :
            restartGame(ptr.get());
            sendFunc = nullptr;
            break;
        case RequestCode::SearchRoom :
            handleSearchRoom(ptr.get(), resMsg);
            break;
        case RequestCode::LeaveRoom :
            handleLeaveRoom(ptr.get(), resMsg);
            sendFunc = nullptr;
            break;
        case RequestCode::GoodBye :
            handleGoodBye(ptr.get());
            sendFunc = nullptr;
            break;
        default:
            break;
    }

    // 序列化[回复数据], 并将[回复数据]发送给客户端
    if (sendFunc) {
        codec.reLoad(&resMsg);
        sendFunc(codec.encodeMsg());
    }
}

// 设置回调: 发送数据的回调 + 释放资源的回调
void Communication::setCallback(sendCallback sendFunc, deleteCallback deleteFunc) {
    _sendMessage = sendFunc;
    _disconnect = deleteFunc;
}


// 登录相关----------------------------------------------------------------
// 处理客户端发来的[Aes密钥分发的Message]
void Communication::handleAesFenFa(Message* reqMsg, Message& resMsg) {
    /*  Aes密钥分发的Message
     *      _data1 = 被Rsa公钥加密的[Aes的密钥]
     *      _data2 = Aes密钥的哈希值 (采用的哈希算法为Sha224)
     *      _reqCode = AesFenFa
     */
    // 读取私钥
    RsaCrypto rsa;
    rsa.parseStringToKey(_redis->RsaSecKey("PrivateKey"), RsaCrypto::PrivateKey);

    // reqMsg._data1 = 被Rsa公钥加密的[Aes的密钥] -> 用Rsa私钥解密_data1, 获取Aes的密钥
    string aesKey = rsa.priKeyDecrypt(reqMsg->_data1);

    // Aes秘钥的哈希校验
    Hash hash(HashType::Sha224);
    hash.addData(aesKey);
    string res = hash.result(); // 16进制数字串格式

    // 根据[哈希校验的结果]准备给客户端回复数据
    /*   Aes密钥校验成功的Message:
     *      _resCode = AesVerifyOk
     *   Aes密钥校验失败的Message:
     *      _data1 = 校验失败的原因
     *      _resCode = AesVerifyFailed
     */
    resMsg._resCode = RespondCode::AesVerifyOk;
    if (reqMsg->_data2 != res) {
        LogDebug("Aes的密钥校验失败!");
        resMsg._data1 = "Aes密钥的哈希校验失败了...";
        resMsg._resCode = RespondCode::Failed;
    }
    else { // 获取到客户端发来的Aes密钥, 就用Aes密钥实例化[Aes算法对象], 之后的收发数据都用Aes进行加解密
        _aes = new AesCrypto(AesCrypto::AES_CBC_256, aesKey);
        LogDebug("Aes的密钥校验成功!");
    }
}

// 处理客户端发来的[注册用户的Message]
void Communication::handleRegister(Message *reqMsg, Message &resMsg) {
    // 查询数据库: select 字段 from 表名 where 条件;
    char sql[1024];
    sprintf(sql, "select * from user where name=%s", reqMsg->_userName.c_str());
    bool flag = _mysql->query(sql);
    LogDebug("flag = %d, sql = %s", flag, sql);

    if (!flag && !_mysql->next()) { // 查询失败 && 结果集为空(用户尚未注册)
        // 将注册信息写入到[user表]
        _mysql->transaction(); // 启动事物
        sprintf(sql, "insert into user (name, passwd, phone, date) values('%s', '%s', '%s', NOW())",
                reqMsg->_userName.data(), reqMsg->_data1.data(), reqMsg->_data2.data());
        bool flag1 = _mysql->update(sql); // 执行insert语句
        LogDebug("flag1 = %d, sql = %s", flag1, sql);

        // 将新用户的属性写入到[information表]
        sprintf(sql, "insert into information (name, score, status) values('%s', 0, 0)",
                reqMsg->_userName.data());
        bool flag2 = _mysql->update(sql); // 执行insert语句
        LogDebug("flag2 = %d, sql = %s", flag2, sql);

        // 若两次写入都成功 -> 提交 | 若有1次写入未成功 -> 事物回滚
        if (flag1 && flag2) {
            _mysql->commit();
            resMsg._resCode = RespondCode::RegisterOk;
        }
        else {
            _mysql->rollback();
            resMsg._resCode = RespondCode::Failed;
            resMsg._data1 = "向数据库插入数据失败!";
        }
    }
    else { // 查询成功 || 结果集不为空 (用户已注册)
        resMsg._resCode = RespondCode::Failed;
        if (!flag) { resMsg._data1 = "查询[user表]时发生错误!"; }
        else { resMsg._data1 = "用户名已存在, 不能重复注册!"; }
    }
}

// 处理客户端发来的[用户登录的Message]
void Communication::handleLogin(Message *reqMsg, Message &resMsg) {
    // 查询数据库: select 字段 from 表名 where 条件;
    char sql[1024];
    /*  SQL语句解析
     *    [1] 3个条件: name = _userName.data()、passwd = _data1.data()、status = 0 (用户未登录)
     *    [2] 当3个条件都满足，才是合法结果
     *    [3] 子查询语句中, *代表[information表]中满足条件的结果, count(*)表示满足条件的结果数量, 当数量不为0时, 整个子查询语句的值为true
     */
    sprintf(sql, "select * from user where name='%s' and passwd='%s' and (select count(*) from information where name='%s' and status='0')"
                    , reqMsg->_userName.data(), reqMsg->_data1.data(), reqMsg->_userName.data());
    bool flag = _mysql->query(sql);
    LogDebug("flag = %d, sql = %s", flag, sql);

    // 查询成功 && 结果集不为空 -> 更改[information表]中用户的登录状态
    if (flag && _mysql->next()) {
        _mysql->transaction(); // 启动事物
        sprintf(sql, "update information set status=1 where name='%s'", reqMsg->_userName.data());
        bool flag1 = _mysql->update(sql);
        LogDebug("flag1 = %d, sql = %s", flag1, sql);
        if (flag1) { // 写入成功
            _mysql->commit(); // 提交
            resMsg._resCode = RespondCode::LoginOk;
            return;
        }
        else { _mysql->rollback(); } // 事物回滚
    }
    resMsg._resCode = RespondCode::Failed;
    resMsg._data1 = "用户名或密码错误, 或者当前用户已经登录了!";
}


// 房间选择----------------------------------------------------------------
/* 处理客户端发来的[加入房间的Message]
 *    [1] _reqCode = AutoRoom / ManualRoom
*/
void Communication::handleAddRoom(Message *reqMsg, Message &resMsg) {
    // 若当前玩家已经不是第一次(本次登录之后的)加入房间 -> 找到该玩家上次加入的房间, 然后将分数读取出来
    string oldRoom = _redis->whereAmI(reqMsg->_userName);        // 获取玩家上次所在的房间名
    int score = _redis->playerScore(oldRoom, reqMsg->_userName); // 记录玩家之前的分数

    if (oldRoom != string()) { // 用户是从上一个房间离开后, 加入到当前房间 -> 让用户离开上个房间
        // 用户离开房间(将用户从Redis中的房间中删除)
        _redis->leaveRoom(oldRoom, reqMsg->_userName);

        // 删除指定房间中的玩家: 若房间中的玩家被全部删除完, 则删除该房间(将用户从RoomList中的房间中删除)
        RoomList::getInstance()->removePlayer(oldRoom, reqMsg->_userName);
    }

    // 玩家加入某个房间
    bool flag = true;
    string roomName; // 记录加入房间的名字
    if (reqMsg->_reqCode == RequestCode::AutoRoom) {
        roomName = _redis->joinRoom(reqMsg->_userName);
    }
    else if (reqMsg->_reqCode == RequestCode::ManualRoom) {
        roomName = reqMsg->_userName;
        flag = _redis->joinRoom(reqMsg->_userName, roomName);
    }

    // 判断是否已经加入到了某个房间中
    if (flag) { // 加入房间成功
        // 玩家的分数: 从MySQL中获取原有分数, 游戏过程中在Redis中不断更新游戏分数, 最后将分数同步到MySQL
        if (score == 0) { // 玩家是本次登录后第一次加入房间 -> 在MySQL中查找该玩家上次登录时的最终分数
            // 查询MySQL, 并将其存储到Redis中
            string sql = "select score from information where name = '" + reqMsg->_userName + "'";
            bool flag1 = _mysql->query(sql);
            assert(flag1);
            _mysql->next();
            score = stoi(_mysql->value(0));
        }
        _redis->updatePlayerScore(roomName, reqMsg->_userName, score);

        // 将房间和玩家的关系保存到单例对象中
        RoomList* roomList = RoomList::getInstance();
        roomList->addUser(roomName, reqMsg->_userName, _sendMessage);

        // 给客户端回复加入房间成功的Message
        resMsg._resCode = RespondCode::JoinRoomOk;
        resMsg._data1 = to_string(_redis->getPlayerCount(roomName));
        resMsg._roomName = roomName;
    }
    else { // 加入房间失败
        resMsg._resCode = RespondCode::Failed;
        resMsg._data1 = "加入房间失败!";
    }
}

// 处理用户离开房间
void Communication::handleLeaveRoom(Message *reqMsg, Message &resMsg) {
    // 将玩家从房间中删除(从Redis和RoomList)
    _redis->leaveRoom(reqMsg->_roomName, reqMsg->_userName);
    RoomList::getInstance()->removePlayer(reqMsg->_roomName, reqMsg->_userName);

    // 组织[给房间内其他玩家]发送的数据: 包含房间中剩余的玩家数
    resMsg._resCode = RespondCode::OtherLeaveRoom;
    auto players = RoomList::getInstance()->getPlayers(reqMsg->_roomName); // 获取房间中的剩余玩家
    resMsg._data1 = to_string(players.size());

    // 将数据发送给[房间内的剩余玩家]
    for (auto& item : players) {
        Codec codec(&resMsg);
        item.second(codec.encodeMsg());
    }
}

// 处理搜索房间
void Communication::handleSearchRoom(Message* reqMsg, Message& resMsg) {
    bool flag = _redis->searchRoom(reqMsg->_roomName);
    resMsg._resCode = RespondCode::SearchRoomOk;
    resMsg._data1 = (flag) ? "true" : "false";
}

// 处理客户端断开连接
void Communication::handleGoodBye(Message* reqMsg) {
    // 修改玩家在服务器的登录状态
    char sql[1024];
    sprintf(sql, "update information set status=0 where name='%s'", reqMsg->_userName.data());
    bool flag = _mysql->update(sql);
    LogDebug("flag = %d, sql = %s, userName = %s", flag, sql, reqMsg->_userName.data());

    // 和客户端断开连接
    _disconnect();
}

// 处理游戏结束
void Communication::handleGameOver(Message* reqMsg) {
    int score = stoi(reqMsg->_data1);
    // 将玩家分数更新到redis
    _redis->updatePlayerScore(reqMsg->_roomName, reqMsg->_userName, score);
    // 将玩家分数更新到mysql
    char sql[1024];
    sprintf(sql, "update information set score='%d' where name='%s'", score, reqMsg->_userName.data());
    _mysql->update(sql);
}

// 准备开始游戏
void Communication::readyForPlay(string roomName, string data) {
    // 获取当前房间的人数
    RoomList* roomList = RoomList::getInstance(); // 取出单例对象
    UserMap players = roomList->getPlayers(roomName);

    // 将房间信息和人数信息发送给各个客户端
    for (auto item : players) {
        item.second(data);
    }

    if (players.size() == 3) { // 当前房间人数已满
        startGame(roomName, players);
    }
}

// 转发数据: data为待转发的数据, roomName为数据的目标房间, userName为[期望将数据转发给房间内其他人]的玩家
void Communication::notifyOtherPlayers(string data, string roomName, string userName) {
    // 得到另外两个玩家
    auto otherPlayers = RoomList::getInstance()->getPartners(roomName, userName);
    for (const auto& item : otherPlayers) {
        item.second(data); // 给房间内的其他玩家发送数据data
    }
}

/* 将[随机顺序的整副牌]发送给三个玩家
 *      [1] _data1 = 除3张底牌之外的其他牌, 格式: 花色-点数#花色-点数#...
 *      [2] _data2 = 剩余的3张底牌, 格式: 花色-点数#花色-点数#...
 *      [3] _resCode = DealCards
*/
void Communication::dealCards(UserMap players) {
    Message msg;
    initCards(); // 洗牌

    // 除3张底牌外的其他牌
    string& all = msg._data1;
    for (int i = 0; i < 51; ++i) {
        auto card = takeOneCard(); // 随机取出1张牌
        string sub = to_string(card.first) + "-" + to_string(card.second) + "#";
        all += sub;
    } // _data1 = 除3张底牌外的其他牌

    // 剩余的3张底牌
    string& lastCards = msg._data2;
    for (const auto& item : _cards) {
        string sub = to_string(item.first) + "-" + to_string(item.second) + "#";
        lastCards += sub;
    } // _data2 = 剩余的3张底牌

    msg._resCode = RespondCode::DealCards;
    Codec codec(&msg);

    // 遍历当前房间中的所有玩家
    for (const auto& player : players) {
        player.second(codec.encodeMsg());
    }
}

// 洗牌
void Communication::initCards() {
    _cards.clear();
    for (int i = 1; i <= 4; ++i) { // 花色(与客户端定义的花色的枚举值相同)
        for (int j = 1; j <= 13; ++j) { // 点数(与客户端定义的点数的枚举值相同)
            _cards.insert(make_pair(i, j));
        }
    }
    _cards.insert(make_pair(0, 14)); // 小王
    _cards.insert(make_pair(0, 15)); // 大王
}

// 随机取出1张牌
pair<int, int> Communication::takeOneCard() {
    // 生成随机数
    random_device rd;  // 创建随机设备对象, 用于生成随机数种子
    mt19937 gen(rd()); // 创建随机数生成对象
    uniform_int_distribution<int> dis(0, _cards.size() - 1); // 创建随机数分布对象 -> 均匀分布
    int randNum = dis(gen); // 生成随机数

    // 遍历map容器
    auto it = _cards.begin();
    for (int i = 0; i < randNum; ++i, ++it);
    _cards.erase(it);
    return *it;
}

// 重新开始游戏
void Communication::restartGame(Message *reqMsg) {
    // 得到房间内的玩家
    auto players = RoomList::getInstance()->getPlayers(reqMsg->_roomName);

    // 判断房间的人数: 若为3, 则清空房间内的玩家
    if (players.size() == 3) {
        RoomList::getInstance()->removeRoom(reqMsg->_roomName); // 将_roomName房间从[单例类RoomList]中删除
    }
    // 将玩家添加到单例对象
    RoomList::getInstance()->addUser(reqMsg->_roomName, reqMsg->_userName, _sendMessage);

    // 发牌并开始游戏
    players = RoomList::getInstance()->getPlayers(reqMsg->_roomName);
    if (players.size() == 3) {
        startGame(reqMsg->_roomName, players);
    }
}

// 开始游戏
void Communication::startGame(string roomName, UserMap players) {
    // 发牌数据
    dealCards(players);

    /* 通知3个客户端可以开始游戏了
     *      [1] _data1 = 所有玩家 + 抢地主次序, 格式: 用户名-次序-分数#用户名-次序-分数#...
     *         ·次序的规则: 谁分数高, 谁优先抢地主
     *      [2] _resCode = StartGame
    */
    Message resMsg;
    resMsg._resCode = RespondCode::StartGame;
    resMsg._data1 = _redis->playersOrder(roomName);
    Codec codec(&resMsg);
    for (const auto& item : players) {
        item.second(codec.encodeMsg());
    }
}



